﻿namespace Funq
{
    using System;
    using System.Collections.Generic;
    using System.Runtime.Serialization;

    public enum ReuseScope
    {
        Container, None
    }

    sealed class ContainerKey
    {
        readonly Type _factoryType;
        readonly Type _serviceType;

        public string Name { get; set; }

        public ContainerKey(Type servicetype, Type factorytype)
        {
            _serviceType = servicetype;
            _factoryType = factorytype;
        }

        public bool Equals(ContainerKey other)
        {
            return ContainerKey.Equals(this, other);
        }

        public override bool Equals(object obj)
        {
            return ContainerKey.Equals(this, obj as ContainerKey);
        }

        public static bool Equals(ContainerKey key1, ContainerKey key2)
        {
            if (Object.Equals(null, key1) || Object.Equals(null, key2)
                || key1.GetType() != key2.GetType())
                return false;
            if (Object.ReferenceEquals(key1, key2)) return true;

            return key1._factoryType == key2._factoryType
                && key1._serviceType == key2._serviceType
                && key1.Name == key2.Name;
        }

        public override int GetHashCode()
        {
            int hash = _factoryType.GetHashCode();
            hash ^= _serviceType.GetHashCode();

            if (Name != null)
                hash ^= Name.GetHashCode();
            return hash;
        }
    }

    sealed class ContainerEntry
    {
        private object _instanceCache;

        public object Factory { get; private set; }
        public ReuseScope Scope { get; set; }

        public ContainerEntry(object factory, ReuseScope scope)
        {
            Factory = factory;
            Scope = scope;
        }

        public TService GetInstance<TFactory, TService>(Func<TFactory, TService> invoker)
        {
            if (Scope == ReuseScope.Container)
            {
                if (_instanceCache == null)
                    _instanceCache = invoker((TFactory)Factory);

                return (TService)_instanceCache;
            }
            else
                return (TService)invoker((TFactory)Factory);
        }
    }

    public sealed class Registration
    {
        internal ContainerKey Key { get; private set; }
        internal ContainerEntry Entry { get; private set; }

        public Registration(Type serviceType, object factory)
        {
            if (serviceType == null)
                throw new ArgumentNullException("serviceType", "serviceType is null.");
            if (factory == null)
                throw new ArgumentNullException("factory", "factory is null.");

            Key = new ContainerKey(serviceType, factory.GetType());
            Entry = new ContainerEntry(factory, ReuseScope.None);
        }

        public void Named(string name)
        {
            Key.Name = name;
        }

        public void Scoped(ReuseScope reuseScope)
        {
            Entry.Scope = reuseScope;
        }

        public override bool Equals(object obj)
        {
            if (obj == null) return false;
            if (!(obj is Registration)) return false;

            Registration other = obj as Registration;
            return this.Key.Equals(other.Key)
                && this.Entry == other.Entry;
        }

        public override int GetHashCode()
        {
            var hash = this.Key.GetHashCode();
            hash ^= this.Entry.GetHashCode();

            return hash;
        }
    }

    public sealed class Container
    {
        Dictionary<ContainerKey, ContainerEntry> _factories = new Dictionary<ContainerKey, ContainerEntry>();

        public TService Resolve<TService>()
        {
            return ResolveImpl<TService, Func<Container, TService>>(null, factory => factory(this), true);
        }

        public TService Resolve<TService, TArg>(TArg arg)
        {
            return ResolveImpl<TService, Func<Container, TArg, TService>>(null, factory => factory(this, arg), true);
        }

        public TService Resolve<TService, TArg1, TArg2>(TArg1 arg1, TArg2 arg2)
        {
            return ResolveImpl<TService, Func<Container, TArg1, TArg2, TService>>(null, factory => factory(this, arg1, arg2), true);
        }

        public TService ResolveNamed<TService>(string name)
        {
            return ResolveImpl<TService, Func<Container, TService>>(name, factory => factory(this), true);
        }

        internal void Register(Registration reg)
        {
            _factories[reg.Key] = reg.Entry;
        }

        public TService TryResolveNamed<TService>(string name)
        {
            return ResolveImpl<TService, Func<Container, TService>>(name, factory => factory(this), false);
        }

        public TService TryResolve<TService>()
        {
            return ResolveImpl<TService, Func<Container, TService>>(null, factory => factory(this), false);
        }

        TService ResolveImpl<TService, TFactory>(string name, Func<TFactory, TService> invoker, bool throwExIfMissing)
        {
            var key = new ContainerKey(typeof(TService), typeof(TFactory)) { Name = name };
            var entry = default(ContainerEntry);

            if (_factories.TryGetValue(key, out entry))
                return entry.GetInstance(invoker);
            
            if (throwExIfMissing)
                throw new ResolutionFailedException();

            return default(TService);
        }
    }

    public sealed class ContainerBuilder
    {
        List<Registration> _registrations = new List<Registration>();

        public Container Build()
        {
            var container = new Container();
            foreach (var registration in _registrations)
                container.Register(registration);
            return container;
        }

        public Registration Register<TService>(Func<Container, TService> factory)
        {
            return RegisterImpl<TService, Func<Container, TService>>(factory);
        }

        public Registration Register<TService, TArg>(Func<Container, TArg, TService> factory)
        {
            return RegisterImpl<TService, Func<Container, TArg, TService>>(factory);
        }

        public Registration Register<TService, TArg1, TArg2>(Func<Container, TArg1, TArg2, TService> factory)
        {
            return RegisterImpl<TService, Func<Container, TArg1, TArg2, TService>>(factory);
        }

        Registration RegisterImpl<TService, TFactory>(TFactory factory)
        {
            try
            {
                var registration = new Registration(typeof(TService), factory);
                _registrations.Add(registration);
                return registration;
            }
            catch (Exception e)
            {
                throw new RegistrationFailedException("Registration Failed unexpectedly", e);
            }
        }
    }

    [Serializable]
    public class ResolutionFailedException : Exception
    {
        public ResolutionFailedException() { }
        public ResolutionFailedException(string message) : base(message) { }
        public ResolutionFailedException(string message, Exception inner) : base(message, inner) { }
        protected ResolutionFailedException(SerializationInfo info, StreamingContext context)
            : base(info, context) { }
    }

    [Serializable]
    public class RegistrationFailedException : Exception
    {
        public RegistrationFailedException() { }
        public RegistrationFailedException(string message) : base(message) { }
        public RegistrationFailedException(string message, Exception inner) : base(message, inner) { }
        protected RegistrationFailedException(
          SerializationInfo info,
          StreamingContext context)
            : base(info, context) { }
    }
}
